package algocity.modelo.juego;

import algocity.modelo.almacenamiento.*;
import algocity.modelo.azar.AzarReal;
import algocity.modelo.catastrofe.Catastrofe;
import algocity.modelo.mapa.Operador;

import java.io.FileNotFoundException;
import java.util.Collection;
import java.util.Map;

public class Juego {

    private FabricaMapa creadorMapa;
    private Jugador jugadorActual;
    private Persistible almacenamiento;

    public Juego(Persistible almacenamiento)
    {
        this(almacenamiento, new FabricaMapaAzar(new AzarReal()));
    }

    public Juego(Persistible almacenamiento, FabricaMapa creadorMapa)
    {
        this.almacenamiento = almacenamiento;
        this.creadorMapa = creadorMapa;
    }

    /**
     * Conocer los puntajes almacenados
     * @return
     * @throws ErrorAlObtenerPuntajes
     */
    public Map<String, Integer> puntajes() throws ErrorAlObtenerPuntajes
    {
        return almacenamiento.getPuntajes();
    }

    /**
     * Agrega un jugador nuevo al juego
     * @param nombre
     * @throws LongitudNombreJugadorInsuficienteException
     * @throws JugadorExistenteException
     * @throws ErrorAlCrearJugadorException
     * @throws JugadorInexistenteException
     */
    public void agregarJugador(String nombre) throws LongitudNombreJugadorInsuficienteException,  JugadorExistenteException, ErrorAlCrearJugadorException, JugadorInexistenteException
    {
        Jugador jugador = crearJugador(nombre);
        almacenamiento.agregarJugador(jugador);
        try
        {
            guardarPuntaje(nombre, Parametros.PUNTAJE_INICIAL_JUGADOR);
        } catch (Throwable e)
        {
            almacenamiento.borrarJugador(nombre);
        }
    }

    private void guardarPuntaje(String nombre, int puntaje)
            throws ErrorAlObtenerPuntajes, FileNotFoundException, GuardarPuntajesException
    {
        Map<String, Integer> puntajes = almacenamiento.getPuntajes();
        puntajes.put(nombre, puntaje);
        almacenamiento.guardarPuntajes(puntajes);
    }


    /**
     * Crea el jugador pasado por parámetro. El jugador no debe existir.
     * @param nombre
     * @return el jugador creado
     * @throws LongitudNombreJugadorInsuficienteException
     */
    private Jugador crearJugador(String nombre) throws LongitudNombreJugadorInsuficienteException {
        return new Jugador(nombre,creadorMapa.crear());
    }

    private void borrarJugador(String nombre) throws JugadorInexistenteException
    {
         almacenamiento.borrarJugador(nombre);
    }

    private Jugador obtenerJugador(String nombre) throws JugadorNoEncontradoException {
        Jugador jugador = null;

        try {
            jugador = almacenamiento.getJugador(nombre);
        } catch (ErrorAlObtenerJugadorException e) {
            throw new JugadorNoEncontradoException();
        }

        return jugador;
    }

    /**
     * Obtener el jugador actual
     * @return el jugador que está jugando
     * @throws JuegoNoIniciadoException
     */
    public Jugador getJugadorActual() throws JuegoNoIniciadoException
    {
        if (jugadorActual == null) {
            throw new JuegoNoIniciadoException();
        }
        return jugadorActual;
    }

    /**
     * Inicia una partida con una nueva ciudad
     * @param nombreUsuario
     * @throws JugadorInexistenteException
     */
    public void iniciarPartida(String nombreUsuario) throws JugadorInexistenteException {
        try {
            obtenerJugador(nombreUsuario);
        } catch (JugadorNoEncontradoException e) {
            throw new JugadorInexistenteException(nombreUsuario);
        }
        try {
            borrarJugador(nombreUsuario);
            agregarJugador(nombreUsuario);
            jugadorActual = obtenerJugador(nombreUsuario);
        } catch (Throwable e) {
            // EL jugador ya fue validado
            // ToDo esto marca un error de lógica interna
        }
    }

    /**
     * Terminación de la ejecución del juego del jugador actual
     */
    public void finJugadorActual() {
        jugadorActual = null;
    }

    /**
     * Continúa la ejecución del juego para el jugador desde el último juego guardado
     * @param nombreUsuario
     * @throws JugadorInexistenteException
     */
    public void continuarPartida(String nombreUsuario) throws JugadorInexistenteException {
        try {
            this.jugadorActual = obtenerJugador(nombreUsuario);
        } catch (JugadorNoEncontradoException e) {
            throw new JugadorInexistenteException(nombreUsuario);
        }
    }

    /**
     * Guarda el juego del jugador que está jugando
     */
    public void guardarJuego()
            throws ErrorAlGuardarJugadorException, JuegoNoIniciadoException, FileNotFoundException,
            GuardarPuntajesException
    {
        Jugador jugadorAntesDeGuardar;
        Map<String, Integer> puntajesAntesDeGuardar;

        if ( getJugadorActual()== null ) {
            throw new JuegoNoIniciadoException();
        }

        Collection<Operador<Object>> deregistrablesAvanceTiempo = getJugadorActual().deregistrarAvanceDeTiempo();
        Collection<Operador<Catastrofe>> deregistrablesAvisoCatastrofe = getJugadorActual().deregistrarAvisoCatastrofe();
        Collection<Operador<String>> deregistrablesAvisoError = getJugadorActual().deregistrarAvisoError();
        Collection<Operador<Object>> deregistrablesAvisoGanador = getJugadorActual().deregistrarAvisoGanador();
        Collection<Operador<Object>> deregistrablesAvisoPerdedor = getJugadorActual().deregistrarAvisoPerdedor();

        try
        {
            try
            {
                jugadorAntesDeGuardar = almacenamiento.getJugador(getJugadorActual().getNombre());
                puntajesAntesDeGuardar = almacenamiento.getPuntajes();
            } catch (Throwable e)
            {
                throw new ErrorAlGuardarJugadorException(jugadorActual.getNombre(), e);
            }

            try
            {
                almacenamiento.guardarJugador(jugadorActual);
                guardarPuntaje(jugadorActual.getNombre(), jugadorActual.getPuntaje());
            } catch (Throwable e)
            {
                almacenamiento.guardarJugador(jugadorAntesDeGuardar);
                almacenamiento.guardarPuntajes(puntajesAntesDeGuardar);
                throw new ErrorAlGuardarJugadorException(jugadorActual.getNombre(), e);
            }
        }
        finally
        {
            registrarAvanceDeTiempo(deregistrablesAvanceTiempo);
            registrarAvisoCatastrofe(deregistrablesAvisoCatastrofe);
            registrarAvisoError(deregistrablesAvisoError);
            registrarAvisoGanador(deregistrablesAvisoGanador);
            registrarAvisoPerdedor(deregistrablesAvisoPerdedor);
        }
    }

    private void registrarAvisoError(Collection<Operador<String>> deregistrables)
            throws JuegoNoIniciadoException
    {
        for ( Operador<String> operador : deregistrables ) {
            getJugadorActual().registrarAvisoError(operador);
        }
    }

    private void registrarAvanceDeTiempo(Collection<Operador<Object>> deregistrables)
            throws JuegoNoIniciadoException
    {
        for ( Operador<Object> operador : deregistrables ) {
            getJugadorActual().registrarAvanceTiempo(operador);
        }
    }

    private void registrarAvisoCatastrofe(Collection<Operador<Catastrofe>> deregistrables)
            throws JuegoNoIniciadoException
    {
        for ( Operador<Catastrofe> operador : deregistrables ) {
            getJugadorActual().registrarAvisoCatastrofe(operador);
        }
    }

    private void registrarAvisoGanador(Collection<Operador<Object>> deregistrables)
            throws JuegoNoIniciadoException
    {
        for ( Operador<Object> operador : deregistrables ) {
            getJugadorActual().registrarAvisoGanador(operador);
        }
    }

    private void registrarAvisoPerdedor(Collection<Operador<Object>> deregistrables)
            throws JuegoNoIniciadoException
    {
        for ( Operador<Object> operador : deregistrables ) {
            getJugadorActual().registrarAvisoPerdedor(operador);
        }
    }

    private class JugadorNoEncontradoException extends Throwable {
    }
}
